#include "StdAfx.h"
#include "MatchScanner.h"
#include "Exceptions.h"
#include "StreamIO.h"

namespace SpikingNeuronLib
{
	// Private Members -------------------------------------------------------------------------------------------

	void MatchScanner::Initialise(int numberOfNeurons, int jitter)
	{
		_fileIndices = nullptr;
		_dataDictionary = nullptr;
		_filesDictionary = nullptr;
		_numberOfNeurons = numberOfNeurons;
		_jitter = jitter;

		// create a list to store added templates
		Templates = gcnew List<MatchTemplate^>();

		_readFileIndex = -1;
		_writeFileIndex = -1;

		_spikeRaster = gcnew array<int, 2>(_numberOfNeurons, RasterTimeAxisLength);
		_offset = RasterOffset0;
	}

	void MatchScanner::InitScanner()
	{
		// create a sorted list of file or data indices
		if (_dataDictionary)
		{
			// _dataDictionary will be null unless AddData() has been called
			_fileIndices = gcnew List<int>(_dataDictionary->Keys);
		}
		else if (_filesDictionary)
		{
			// _filesDictionary may be null depending on the MatchScanner constructor and whether AddFiles() has been called
			_fileIndices = gcnew List<int>(_filesDictionary->Keys);
		}
		else
		{
			throw gcnew NoDataSourceException("No data source. Add files or add data before calling Match().");
		}

		_fileIndices->Sort();

		// create a sequential mapping of the file indices list
		_sequentialLookup = gcnew Dictionary<int, int>();
		int sequentialIndex = 0;
		for (int i = 0; i < _fileIndices->Count; i++)
		{
			int fileIndex = _fileIndices[i];
			_sequentialLookup->Add(fileIndex, sequentialIndex++);
		}
	}

	inline void MatchScanner::MatchAllTemplates(String^ tag)
	{
		List<MatchTemplate^>::Enumerator ^enumerator = Templates->GetEnumerator();
		while (enumerator->MoveNext())
		{
			MatchTemplate^ pattern = enumerator->Current;
			this->MatchOneTemplate(pattern, _offset, tag);
		}
	}

	inline void MatchScanner::MatchOneTemplate(MatchTemplate^ pattern, int offset, String^ tag)
	{
		for (int i = 0; i < RasterSegmentSize; i++)
		{
			int index = offset + i;	// wrap from one segment to the next
			pattern->Match(tag, _spikeRaster, index, i);
		}

		pattern->AggregationFlush(tag);
	}

	void MatchScanner::HandleAggregatedMatch(String^ tag, int templateID, int uniqueID, int timeIndex, int matchCount, String^ matchPattern, array<FiringEvent>^ coincidentFiringEvents)
	{
		_nMatches++;
		Matched(MatchResult(tag, templateID, uniqueID, _readFileIndex, timeIndex, matchCount, matchPattern, coincidentFiringEvents));
	}

	long MatchScanner::GetLastAccess(int i)
	{
		return _fileCache[i]->LastAccess;
	}

	int MatchScanner::GetFileIndex(int currentFileIndex, int requiredOffset)
    {
		// Given a current file index, return either the next or previous indices.
		// 'requiredOffset': +1 or -1 (depending on whether the next or previous index is required)
        // 'compareOffset': either the minimum or maximum index (depending on 'requiredOffset')
        int compareOffset = requiredOffset > 0 ? _fileIndices->Count - 1 : 0;
        int requiredIndex;

        if (currentFileIndex != _fileIndices[compareOffset])
            requiredIndex = _fileIndices[_sequentialLookup[currentFileIndex] + requiredOffset];
        else
            requiredIndex = -1;

        return requiredIndex;
    }

	List<FiringEvent>^ MatchScanner::GetEventData(String^ filePath)
	{
		const int N_firings_max = 100000;
		List<FiringEvent>^ firings = gcnew List<FiringEvent>(N_firings_max);

		using namespace System::IO;
		try
		{
			StreamReader^ sr = File::OpenText(filePath);
			try
			{
				while (sr->Peek() >= 0)
				{
					int time = StreamIO::ScanStreamForType<int>(sr);
					int nindex = StreamIO::ScanStreamForType<int>(sr);
					if (time >= 0)	// valid firing data has times in the range 0 to 999 inclusive (other data is overflow from the previous second)
					{
						firings->Add(FiringEvent(time, nindex, EventLabel::Unspecified));
					}
				}
			}
			finally
			{
				if (sr)
					delete (IDisposable^) sr;
			}
		}
		catch (Exception^ ex)
		{
			String^ message = String::Format("Error in opening or reading file {0}", filePath);
			throw gcnew FileIOException(message, ex);
		}

		return firings;
	}

	List<FiringEvent>^ MatchScanner::GetFirings(int fileIndex)
	{
		if (!_fileCache)
		{
			_fileCache = gcnew Dictionary<int, CacheData^>();
		}

		List<FiringEvent>^ firings;
		if (_fileCache->ContainsKey(fileIndex))
		{
			firings = _fileCache[fileIndex]->FiringData;
			_fileCache[fileIndex]->LastAccess = (long) DateTime::Now.Ticks;		// truncate to long
		}
		else
		{
			String^ filePath = _filesDictionary[fileIndex];
			firings = GetEventData(filePath);
			_fileCache[fileIndex] = gcnew CacheData(firings, (long) DateTime::Now.Ticks);	// truncate to long

			// remove oldest entries
			if (_fileCache->Count > MaxCacheSize)
			{
				// can't use lambda in CLR!!
				//[=](int i) { return _fileCache[i]->LastAccess; };

				// get an array of keys sorted by last access
				Func<int, long>^ keySelector = gcnew Func<int, long>(this, &MatchScanner::GetLastAccess);
				using namespace System::Linq;
				array<int>^ sortedKeys = Enumerable::ToArray<int>(Enumerable::OrderBy(_fileCache->Keys, keySelector));

				// grab the first two keys and remove them
				_fileCache->Remove(sortedKeys[0]);
				_fileCache->Remove(sortedKeys[1]);
			}
		}

		return firings;
	}

	void MatchScanner::CreateSpikeRaster(List<FiringEvent>^ firings)
	{
		this->ClearSpikeRaster(_offset);
		this->FillSpikeRaster(firings, _spikeRaster, _offset);

		if (_offset == RasterOffset0)
			_offset = RasterOffset1;
		else
			_offset = RasterOffset0;
	}

	void MatchScanner::FillSpikeRaster(List<FiringEvent>^ firings, array<int, 2>^ spikeRaster, int offset)
	{
		List<FiringEvent>::Enumerator ^enumerator = firings->GetEnumerator();
		while (enumerator->MoveNext())
		{
			FiringEvent firingEvent = enumerator->Current;
			if (firingEvent.Time < RasterSegmentSize)	// assume that firings extend for less than 1000 milliseconds
				spikeRaster[firingEvent.NeuronIndex, offset + firingEvent.Time] = 1;
		}
	}

	void MatchScanner::ClearSpikeRaster(int offset)
	{
		for (int i = 0; i < _numberOfNeurons; i++)
		{
			for (int j = offset; j < offset + RasterSegmentSize; j++)
			{
				_spikeRaster[i, j] = 0;
			}
		}
	}

	// Public Members -------------------------------------------------------------------------------------------

	MatchScanner::MatchScanner(int numberOfNeurons, int jitter)
	{
		// Simplified interface for use with Python
		this->Initialise(numberOfNeurons, jitter);
	}

	MatchScanner::MatchScanner(Dictionary<int, String^>^ filesDictionary, int numberOfNeurons, int jitter)
	{
		this->Initialise(numberOfNeurons, jitter);

		_filesDictionary = filesDictionary;		// a mapping between file indices and their corresponding full file paths
		this->InitScanner();
	}

	void MatchScanner::Reset()
	{
		// call Reset() before matching against new data e.g. before a new in-memory scan
		this->Initialise(_numberOfNeurons, _jitter);
	}

	void MatchScanner::SaveData(String^ filePath)
	{
		// each firings list is assumed to cover one second
		// the firings lists in _dataDictionary are assumed to be sequential
		// i.e. run on from one second to the next
		using namespace System::IO;
		StreamWriter^ file = gcnew StreamWriter(filePath);

		List<int>^ keys = gcnew List<int>(_dataDictionary->Keys);
		keys->Sort();
		int base = 0;
		for each (int key in keys)
		{
			List<FiringEvent>^ firings = _dataDictionary[key];
			for each (FiringEvent fe in firings)
			{
				file->WriteLine(String::Format("{0} {1}", base + fe.Time, fe.NeuronIndex));
			}

			base += 1000;	// add one second to the base firing time
		}

		// TODO the MatchScanner can't reload this file in its entirety; only the first 1000 msec will be loaded

		file->Close();
	}

	void MatchScanner::SaveData(String^ filePath, int fileIndex)
	{
		using namespace System::IO;
		StreamWriter^ file = gcnew StreamWriter(filePath);

		List<FiringEvent>^ firings = _dataDictionary[fileIndex];
		for each (FiringEvent fe in firings)
		{
			file->WriteLine(String::Format("{0} {1}", fe.Time, fe.NeuronIndex));
		}

		file->Close();
	}

	void MatchScanner::AddData(int fileIndex, List<FiringEvent>^ data)
	{
		if (_dataDictionary == nullptr)
		{
			_dataDictionary = gcnew Dictionary<int, List<FiringEvent>^>();
		}

		// add one second of firing data to the data dictionary (used for in-memory scans)
		_dataDictionary->Add(fileIndex, data);
	}

	void MatchScanner::AddFile(int fileIndex, String^ filePath)
	{
		if (_filesDictionary == nullptr)
		{
			_filesDictionary = gcnew Dictionary<int, String^>();
		}

		// add the path to a file containing one second of firing data
		_filesDictionary->Add(fileIndex, filePath);
	}

	void MatchScanner::AddTemplate(Pattern^ pattern, int matchThreshold)
	{
		int nextIndex = Templates->Count;
		Templates->Add(gcnew MatchTemplate(nextIndex, pattern, _jitter, matchThreshold));
	}

	int MatchScanner::AddTemplate(String^ firingsString, int matchThreshold)
    {
		Pattern^ firings = Pattern::ConvertFromString(firingsString);

		if (firings->Count)
		{
			this->AddTemplate(firings, matchThreshold);
		}

		return firings->Count;
    }

	void MatchScanner::ClearTemplates()
	{
		List<MatchTemplate^>::Enumerator ^enumerator = Templates->GetEnumerator();
		while (enumerator->MoveNext())
		{
			MatchTemplate^ pattern = enumerator->Current;
			delete pattern;
		}

		Templates->Clear();
	}

	void MatchScanner::Match(String^ tag)
	{
		if (Templates->Count == 0)
		{
			throw gcnew NoTemplatesException("No templates to match");
		}
		else
		{
			// preallocate an aggregator for each template
			AggregatedMatchEventHandler^ matchHandler = gcnew AggregatedMatchEventHandler(this, &MatchScanner::HandleAggregatedMatch);
			List<MatchTemplate^>::Enumerator ^enumerator = Templates->GetEnumerator();
			while (enumerator->MoveNext())
			{
				MatchTemplate^ pattern = enumerator->Current;
				pattern->AddMatchHandler(matchHandler);
			}

			if (_fileIndices == nullptr)
			{
				this->InitScanner();
			}

			_nMatches = 0;
			for (int i = 0; i < _fileIndices->Count; i++)
			{
				_readFileIndex = _writeFileIndex;
				_writeFileIndex = _fileIndices[i];

				// write to write segment
				List<FiringEvent>^ firings;
				if (_dataDictionary)
				{
					firings = _dataDictionary[_writeFileIndex];
				}
				else
				{
					String^ filePath = _filesDictionary[_writeFileIndex];
					firings = GetEventData(filePath);
				}
				CreateSpikeRaster(firings);

				if (i > 0)	// require two seconds of data before starting
				{
					// read from read segment
					this->MatchAllTemplates(tag);
				}

				ScanUpdate(_fileIndices[i], _nMatches);
			}

			// last segment
			this->ClearSpikeRaster(_offset);
			_readFileIndex = _writeFileIndex;
			if (_offset == RasterOffset0)
				_offset = RasterOffset1;
			else
				_offset = RasterOffset0;

			this->MatchAllTemplates(tag);
		}
	}

	MatchRaster MatchScanner::GetMatch(MatchResult result)
	{
		return this->GetMatch(result.TemplateID, result.Index, result.Offset);
	}

	MatchRaster MatchScanner::GetMatch(int templateID, int fileIndex, int offset)
	{
		MatchTemplate^ pattern = Templates[templateID];

		// retrieve the matching file data...
		List<FiringEvent>^ firings = this->GetFirings(fileIndex);

		// ...and create a full spike raster
		array<int, 2>^ spikeRaster;
		if (offset + pattern->Timespan < RasterSegmentSize)
		{
			spikeRaster = gcnew array<int, 2>(_numberOfNeurons, RasterSegmentSize);
			this->FillSpikeRaster(firings, spikeRaster, RasterOffset0);
		}
		else
		{
			// pattern overlaps the boundary between segments
			// create a raster of both the current and the following files
			spikeRaster = gcnew array<int, 2>(_numberOfNeurons, RasterTimeAxisLength);
			this->FillSpikeRaster(firings, spikeRaster, RasterOffset0);
			int nextFileIndex = this->GetFileIndex(fileIndex, 1);
			if (nextFileIndex < 0)
			{
				throw gcnew NoDataSourceException("The pattern overlaps the following file. Add the following file before calling GetMatch().");
			}
			else
			{
				firings = this->GetFirings(nextFileIndex);
				this->FillSpikeRaster(firings, spikeRaster, RasterOffset1);
			}
		}

		// get the match as a pair of rasters for data and template
		return pattern->GetMatch(spikeRaster, offset);
	}

	int MatchScanner::ScoreMatch(MatchRaster match)
	{
		// return the number of exact matches for this match
		// for a perfect score, the score is equal to the number of firing events in the template
        array<array<int>^>^ dataMatch = match.DataMatch;
        array<array<int>^>^ templateMatch = match.TemplateMatch;
		int count = 0;
        for (int i = 0; i < dataMatch->Length; i++)
        {
            System::Diagnostics::Debug::Assert(dataMatch[i][0] == templateMatch[i][0]);
            for (int j = 1; j < dataMatch[i]->Length; j++)
            {
                if (dataMatch[i][j] == 1 && templateMatch[i][j] == 1)
                {
                    count++;
                }
            }
        }

		return count;
	}
}